/*
 * @Copyright: All rights reserved. Please Contact the Email sagittar_zhang@126.com
 */

package cn.guet.base.spider.http;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Random;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;

import org.apache.commons.lang3.StringUtils;
import org.apache.http.Consts;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpRequest;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.ConnectionConfig;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.config.SocketConfig;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import cn.guet.base.spider.Constants;

/**
 * 网络请求工具
 *
 * @author SAGITTAR
 */
public final class HttpClientUtil {

    private static final Logger LOGGER = LogManager.getLogger(HttpClientUtil.class);

    private static RequestConfig requestConfig;
    private static CloseableHttpClient httpClient;

    static {
        try {
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(keyStore, (chain, authType) -> true).build();
            SSLConnectionSocketFactory sslFactory = new SSLConnectionSocketFactory(sslContext);
            RegistryBuilder<ConnectionSocketFactory> registryBuilder = RegistryBuilder.create();
            registryBuilder.register("http", PlainConnectionSocketFactory.INSTANCE);
            registryBuilder.register("https", sslFactory);
            Registry<ConnectionSocketFactory> socketFactoryRegistry = registryBuilder.build();

            PoolingHttpClientConnectionManager manager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);

            SocketConfig.Builder builder = SocketConfig.custom();
            manager.setDefaultSocketConfig(builder.setSoTimeout(Constants.TIMEOUT).setTcpNoDelay(true).build());
            ConnectionConfig.Builder connectionConfigBuilder = ConnectionConfig.custom();
            connectionConfigBuilder = connectionConfigBuilder.setCharset(Consts.UTF_8);
            connectionConfigBuilder.setMalformedInputAction(CodingErrorAction.IGNORE);
            connectionConfigBuilder.setUnmappableInputAction(CodingErrorAction.IGNORE);
            ConnectionConfig connectionConfig = connectionConfigBuilder.build();
            manager.setDefaultConnectionConfig(connectionConfig);
            manager.setMaxTotal(500);
            manager.setDefaultMaxPerRoute(300);
            HttpClientBuilder httpClientBuilder = HttpClients.custom().setConnectionManager(manager);
            httpClientBuilder.setDefaultCookieStore(new BasicCookieStore()).setUserAgent(Constants.USER_AGENT_ARRAY[0]);
            httpClientBuilder.setRetryHandler((exception, executionCount, context) -> {
                if (executionCount > 2) {
                    return false;
                }
                if (exception instanceof InterruptedIOException) {
                    return true;
                }
                if (exception instanceof UnknownHostException) {
                    return true;
                }
                if (exception instanceof SSLException) {
                    return true;
                }
                HttpRequest request = HttpClientContext.adapt(context).getRequest();
                return !(request instanceof HttpEntityEnclosingRequest);
            });
            httpClient = httpClientBuilder.build();
            RequestConfig.Builder requestConfigBuilder = RequestConfig.custom();
            requestConfigBuilder = requestConfigBuilder.setSocketTimeout(Constants.TIMEOUT);
            requestConfigBuilder.setConnectTimeout(Constants.TIMEOUT);
            requestConfigBuilder.setConnectionRequestTimeout(Constants.TIMEOUT);
            requestConfigBuilder.setCookieSpec(CookieSpecs.STANDARD);
            requestConfig = requestConfigBuilder.build();
        } catch (Exception e) {
            LOGGER.error(e.getLocalizedMessage(), e);
        }
    }

    private HttpClientUtil() {
        throw new AssertionError();
    }

    /**
     * 获取网页源代码，默认编码UTF-8
     *
     * @param url 请求地址
     * @return 网页源代码
     * @throws IOException 响应异常
     */
    public static String getWebPage(String url) throws IOException {
        return getWebPage(new HttpGet(url), StandardCharsets.UTF_8);
    }

    /**
     * 以指定的编码获取网页源代码
     *
     * @param url 请求地址
     * @param encoding 编码格式
     * @return 网页源代码
     * @throws IOException 响应异常
     */
    public static String getWebPage(String url, Charset encoding) throws IOException {
        return getWebPage(new HttpGet(url), encoding);
    }

    /**
     * 返回网页内容，默认编码UTF-8
     *
     * @param request 请求对象
     * @return 网页源代码
     * @throws IOException 响应异常
     */
    public static String getWebPage(HttpRequestBase request) throws IOException {
        return getWebPage(request, StandardCharsets.UTF_8);
    }

    /**
     * 返回网页内容
     *
     * @param request 请求基类
     * @param encoding 编码格式
     * @return 网页源代码
     * @throws IOException 响应异常
     */
    public static String getWebPage(HttpRequestBase request, Charset encoding) throws IOException {
        CloseableHttpResponse response = getResponse(request);
        String content = EntityUtils.toString(response.getEntity(), encoding);
        request.releaseConnection();
        return content;
    }

    /**
     * HttpPost请求
     *
     * @param postUrl 请求地址
     * @param params 请求体参数
     * @return 网页源代码
     * @throws IOException 响应异常
     */
    public static String postRequest(String postUrl, Map<String, String> params) throws IOException {
        List<NameValuePair> formParams = new ArrayList<>(params.size());
        for (Map.Entry<String, String> entry : params.entrySet()) {
            formParams.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
        }
        HttpPost httpPost = new HttpPost(postUrl);
        httpPost.setEntity(new UrlEncodedFormEntity(formParams, StandardCharsets.UTF_8));
        return getWebPage(httpPost, StandardCharsets.UTF_8);
    }

    public static CloseableHttpResponse getResponse(HttpRequestBase request) throws IOException {
        if (Objects.isNull(request.getConfig())) {
            request.setConfig(requestConfig);
        }
        int randomIndex = new Random().nextInt(Constants.USER_AGENT_ARRAY.length);
        request.setHeader("User-Agent", Constants.USER_AGENT_ARRAY[randomIndex]);
        CloseableHttpResponse response = httpClient.execute(request, HttpClientContext.create());
        int statusCode = response.getStatusLine().getStatusCode();
        if (statusCode != HttpStatus.SC_OK) {
            throw new IOException("status code is: " + statusCode);
        }
        return response;
    }

    public static CloseableHttpResponse getResponse(String url) throws IOException {
        return getResponse(new HttpGet(url));
    }

    /**
     * 优化URL
     *
     * @param url 原始URL
     * @return 优化URL
     */
    public static String validUrl(String url) {
        if (StringUtils.startsWith(url, "//")) {
            url = StringUtils.substring(url, 2);
        }
        if (!StringUtils.startsWith(url, "http://")) {
            url = StringUtils.join("http://", url);
        }
        return url;
    }

    /**
     * 下载图片
     *
     * @param fileUrl 文件地址
     * @param path 保存路径
     * @param saveFileName 文件名
     * @param isReplaceFile 若存在文件时，是否还需要下载文件
     */
    public static void downloadFile(String fileUrl, String path, String saveFileName, Boolean isReplaceFile) {
        File directory = new File(path);
        // 如果文件夹不存在则创建
        if (!directory.exists() && !directory.isDirectory()) {
            boolean result = directory.mkdirs();
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("directory -> {}", result);
            }
        }
        if (!StringUtils.contains(saveFileName, ".")) {
            String extension = StringUtils.substring(fileUrl, StringUtils.lastIndexOf(fileUrl, "."));
            saveFileName = StringUtils.join(saveFileName, extension);
        }
        File destFile = new File(Paths.get(path).resolve(saveFileName).toUri());
        if (destFile.exists() && !isReplaceFile) {
            return;
        }

        // 如果文件不存在或需要替换，则下载
        try (CloseableHttpResponse response = getResponse(fileUrl)) {
            try (InputStream inputStream = response.getEntity().getContent()) {
                try (OutputStream outputStream = new FileOutputStream(destFile)) {
                    byte[] temp = new byte[10 * 1024];
                    int currentRead;
                    while ((currentRead = inputStream.read(temp)) != -1) {
                        outputStream.write(temp, 0, currentRead);
                    }
                    LOGGER.info("{} -> 文件成功下载至 -> {}", fileUrl, destFile.getCanonicalPath());
                }
            }
        } catch (IOException | UnsupportedOperationException e) {
            LOGGER.error(e.getLocalizedMessage(), e);
        }
    }
}
